﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Specialized;
using System.Web;
using System.Net;
using System.IO;
using System.Threading;
using System.Security.Cryptography;

namespace DLV.Library.Utilities
{
    public class OAuthYahoo : OAuthBase
    {
        private string _consumerKey = "";
        private string _consumerSecret = "";
        private string _token = "";
        private string _tokenSecret = "";
        private string _sessionHandle = "";
        private string _callback = "";
        private string oauth_verifier = "";
        private string _guid = "";
        private static DateTime lastConnectTime = DateTime.MinValue;
        private int MinimumMillisecondsBetweenQueries = 1000;
        private int MaxRetries = 3;

        #region PublicPropertiies
        public string ConsumerKey { get { return _consumerKey; } set { _consumerKey = value; } }
        public string ConsumerSecret { get { return _consumerSecret; } set { _consumerSecret = value; } }
        public string Token { get { return _token; } set { _token = value; } }
        public string TokenSecret { get { return _tokenSecret; } set { _tokenSecret = value; } }
        public string SessionHandle { get { return _sessionHandle; } set { _sessionHandle = value; } }
        public string Callback { get { return _callback; } set { _callback = value; } }       
        public string Verifier { get { return oauth_verifier; } set { oauth_verifier = value; } }
        public string Guid { get { return _guid; } set { _guid = value; } }
        #endregion

        public enum Method { GET, POST, PUT, DELETE };
        public const string USER_AGENT = "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.19) Gecko/2010031422 Firefox/3.0.19 (.NET CLR 3.5.30729)";
        public const string REQUEST_TOKEN = "https://api.login.yahoo.com/oauth/v2/get_request_token";
        public const string AUTHORIZE = "https://api.login.yahoo.com/oauth/v2/request_auth";
        public const string ACCESS_TOKEN = "https://api.login.yahoo.com/oauth/v2/get_token";
        public const string API_BASE_URL = "http://social.yahooapis.com/v1/";

        public OAuthYahoo(string consumerKey, string consumerSecret, string callback)
        {
            this.ConsumerKey = consumerKey;
            this.ConsumerSecret = consumerSecret;
            this.Callback = callback;
        }

        public void GetRequestToken()
        {
            string response = oAuthWebRequest(Method.POST, REQUEST_TOKEN, String.Empty);
            if (response.Length > 0)
            {
                NameValueCollection qs = HttpUtility.ParseQueryString(response);
                if (qs["oauth_token"] != null)
                {
                    this.Token = qs["oauth_token"];
                    this.TokenSecret = qs["oauth_token_secret"];
                }
            }
        }

        public string AuthorizationLink
        {
            get { return AUTHORIZE + "?oauth_token=" + this.Token; }
        }

        public void GetAccessToken()
        {
            if (string.IsNullOrEmpty(Token) || string.IsNullOrEmpty(Verifier))
            {
                Exception e = new Exception("The request token and verifier were not set");
                throw e;
            }

            string response = oAuthWebRequest(Method.POST, ACCESS_TOKEN, string.Empty);

            if (response.Length > 0)
            {
                NameValueCollection qs = HttpUtility.ParseQueryString(response);
                if (qs["oauth_token"] != null)
                {
                    this.Token = qs["oauth_token"];
                }
                if (qs["oauth_token_secret"] != null)
                {
                    this.TokenSecret = qs["oauth_token_secret"];
                }
                if (qs["oauth_session_handle"] != null)
                {
                    this.SessionHandle = qs["oauth_session_handle"];
                }
                if (qs["xoauth_yahoo_guid"] != null)
                {
                    this.Guid = qs["xoauth_yahoo_guid"];
                }
            }
        }

        public void RefreshAccessToken()
        {
            if (string.IsNullOrEmpty(this.Token) || string.IsNullOrEmpty(this.SessionHandle))
            {
                Exception e = new Exception("The request token and session handle were not set");
                throw e;
            }

            string nonce = this.GenerateNonce();
            string timeStamp = this.GenerateTimeStamp();
            string url = ACCESS_TOKEN + "?oauth_consumer_key=" + this.ConsumerKey + "&oauth_nonce=" + nonce + "&oauth_signature_method=plaintext&oauth_timestamp=" + timeStamp + "&oauth_token=" + this.Token + "&oauth_version=1.0&oauth_signature=" + this.ConsumerSecret + "%26" + this.TokenSecret + "&oauth_session_handle=" + this.SessionHandle;

            HttpWebRequest webRequest = null;
            webRequest = System.Net.WebRequest.Create(url) as HttpWebRequest;
            webRequest.Method = "GET";
            webRequest.Credentials = CredentialCache.DefaultCredentials;
            webRequest.AllowWriteStreamBuffering = true;

            string response = WebResponseGet(webRequest, 0);

            if (response.Length > 0)
            {
                NameValueCollection qs = HttpUtility.ParseQueryString(response);
                if (qs["oauth_token"] != null)
                {
                    this.Token = qs["oauth_token"];
                }
                if (qs["oauth_token_secret"] != null)
                {
                    this.TokenSecret = qs["oauth_token_secret"];
                }
                if (qs["oauth_session_handle"] != null)
                {
                    this.SessionHandle = qs["oauth_session_handle"];
                }
            }
        }

        public string APIWebRequest(Method method, string relativeUrl, string postData)
        {
            int millisecondsBetweenQueries = MinimumMillisecondsBetweenQueries;

            TimeSpan span = System.DateTime.Now - lastConnectTime.AddMilliseconds(millisecondsBetweenQueries);
            if (span.TotalMilliseconds < 0)
                Thread.Sleep(Math.Abs((int)span.TotalMilliseconds));

            //remove '?' or '&' from end of the relative url
            relativeUrl = System.Text.RegularExpressions.Regex.Replace(relativeUrl, @"[\?|&]$", "");

            string fullUrl = API_BASE_URL + relativeUrl;

            Uri uri = new Uri(fullUrl);
            string nonce = this.GenerateNonce();
            string timeStamp = this.GenerateTimeStamp();

            string outUrl, querystring, encodedToken;

            encodedToken = UrlEncode(this.Token);

            //verifier needs to be excluded from signature for API call
            this.Verifier = "";

            //Generate Signature
            string sig = this.YahooGenerateSignature(uri,
                this.ConsumerKey,
                this.ConsumerSecret,
                encodedToken,
                this.TokenSecret,
                method.ToString(),
                timeStamp,
                nonce,
                null,
                SignatureTypes.HMACSHA1,
                out outUrl,
                out querystring);

            HttpWebRequest webRequest = null;

            webRequest = System.Net.WebRequest.Create(fullUrl) as HttpWebRequest;
            webRequest.Method = method.ToString();
            webRequest.Credentials = CredentialCache.DefaultCredentials;
            webRequest.AllowWriteStreamBuffering = true;

            webRequest.PreAuthenticate = true;
            webRequest.ServicePoint.Expect100Continue = false;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;

            webRequest.Headers.Add("Authorization", "OAuth realm=\"yahooapis.com\",oauth_consumer_key=\"" + this.ConsumerKey + "\",oauth_nonce=\"" + nonce + "\",oauth_signature_method=\"HMAC-SHA1\",oauth_timestamp=\"" + timeStamp + "\",oauth_token=\"" + encodedToken + "\",oauth_version=\"1.0\",oauth_signature=\"" + sig + "\"");

            if (postData != null)
            {
                byte[] fileToSend = Encoding.UTF8.GetBytes(postData);
                webRequest.ContentLength = fileToSend.Length;

                Stream reqStream = webRequest.GetRequestStream();

                reqStream.Write(fileToSend, 0, fileToSend.Length);
                reqStream.Close();
            }

            return WebResponseGet(webRequest, 0);
        }

        private string WebResponseGet(HttpWebRequest webRequest, int callCount)
        {
            int millisecondsBetweenQueries = MinimumMillisecondsBetweenQueries + (1000 * callCount * callCount);

            TimeSpan span = System.DateTime.Now - lastConnectTime.AddMilliseconds(millisecondsBetweenQueries);
            if (span.TotalMilliseconds < 0)
                Thread.Sleep(Math.Abs((int)span.TotalMilliseconds));

            StreamReader responseReader = null;
            HttpWebResponse response = null;
            string responseData = "";

            try
            {
                response = (HttpWebResponse)webRequest.GetResponse();
                Stream receiveStream = response.GetResponseStream();
                responseReader = new StreamReader(receiveStream, Encoding.UTF8);
                responseData = responseReader.ReadToEnd();

                lastConnectTime = System.DateTime.Now;
            }
            catch (WebException e)
            {
                if (e.Status == WebExceptionStatus.ProtocolError || e.Status == WebExceptionStatus.Timeout)
                {
                    HttpWebResponse webResponse = e.Response as HttpWebResponse;
                    if (webResponse != null)
                    {
                        // del.icio.us servers seem to have less-than-ideal response times quite often.
                        // we try to compensate for those probelms, but eventually error out.
                        if (e.Status == WebExceptionStatus.Timeout
                            || webResponse.StatusCode == HttpStatusCode.ServiceUnavailable /*503 we have been throttled*/
                            || (int)webResponse.StatusCode == 999 /*Unable to process request at this time*/)
                        {
                            if (callCount < MaxRetries) // don't loop endlessly here, eventually just error out
                                return WebResponseGet(webRequest, callCount + 1);
                            else
                                throw new Exception(String.Format("The server is not responding.\t{0}", webResponse.StatusCode));
                        }
                        else if (webResponse.StatusCode == HttpStatusCode.Unauthorized /*401*/)
                        {
                            throw new Exception(e.Message);
                        }
                        else
                        {
                            throw new Exception(webResponse.StatusCode.ToString());
                        }
                    }
                }

                throw new Exception(e.Status.ToString());
            }
            catch (IOException e)
            {
                // Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
                throw new Exception(e.ToString());
            }
            finally
            {
                if (response != null)
                    response.Close();
                if (responseReader != null)
                    responseReader.Close();
            }

            return responseData;
        }

        private string oAuthWebRequest(Method method, string url, string postData)
        {
            string outUrl = "";
            string querystring = "";
            string ret = "";

            //Setup postData for signing.
            //Add the postData to the querystring.
            if (method == Method.POST)
            {
                if (postData.Length > 0)
                {
                    //Decode the parameters and re-encode using the oAuth UrlEncode method.
                    NameValueCollection qs = HttpUtility.ParseQueryString(postData);
                    postData = "";
                    foreach (string key in qs.AllKeys)
                    {
                        if (postData.Length > 0)
                        {
                            postData += "&";
                        }
                        qs[key] = HttpUtility.UrlDecode(qs[key]);
                        qs[key] = this.UrlEncode(qs[key]);
                        postData += key + "=" + qs[key];

                    }
                    if (url.IndexOf("?") > 0)
                    {
                        url += "&";
                    }
                    else
                    {
                        url += "?";
                    }
                    url += postData;
                }
            }

            Uri uri = new Uri(url);

            string nonce = this.GenerateNonce();
            string timeStamp = this.GenerateTimeStamp();

            string callback = "";
            if (url.ToString().Contains(REQUEST_TOKEN))
                callback = this.Callback;
            
            //Generate Signature
            string sig = this.YahooGenerateSignature(uri,
                this.ConsumerKey,
                this.ConsumerSecret,
                this.Token,
                this.TokenSecret,
                method.ToString(),
                timeStamp,
                nonce,  
                callback,
                SignatureTypes.HMACSHA1,
                out outUrl,
                out querystring);


            querystring += "&oauth_signature=" + HttpUtility.UrlEncode(sig);

            //Convert the querystring to postData
            if (method == Method.POST)
            {
                postData = querystring;
                querystring = "";
            }

            if (querystring.Length > 0)
            {
                outUrl += "?";
            }

            if (method == Method.POST || method == Method.GET)
                ret = WebRequest(method, outUrl + querystring, postData);

            return ret;
        }

        private string WebRequest(Method method, string url, string postData)
        {
            HttpWebRequest webRequest = null;
            StreamWriter requestWriter = null;
            string responseData = "";

            webRequest = System.Net.WebRequest.Create(url) as HttpWebRequest;
            webRequest.Method = method.ToString();
            webRequest.ServicePoint.Expect100Continue = false;
            webRequest.UserAgent = USER_AGENT;
            webRequest.Timeout = 20000;

            if (method == Method.POST)
            {
                webRequest.ContentType = "application/x-www-form-urlencoded";

                requestWriter = new StreamWriter(webRequest.GetRequestStream());
                try
                {
                    requestWriter.Write(postData);
                }
                catch
                {
                    throw;
                }
                finally
                {
                    requestWriter.Close();
                    requestWriter = null;
                }
            }

            responseData = WebResponseGet(webRequest, 0);

            webRequest = null;

            return responseData;

        }

        private string YahooGenerateSignature(Uri url, string consumerKey, string consumerSecret, string token, string tokenSecret, string httpMethod, string timeStamp, string nonce, string callback, SignatureTypes signatureType, out string normalizedUrl, out string normalizedRequestParameters)
        {
            normalizedUrl = null;
            normalizedRequestParameters = null;

            switch (signatureType)
            {
                case SignatureTypes.PLAINTEXT:
                    return HttpUtility.UrlEncode(string.Format("{0}&{1}", consumerSecret, tokenSecret));
                case SignatureTypes.HMACSHA1:
                    string signatureBase = GenerateSignatureBase(url, consumerKey, token, tokenSecret, httpMethod, timeStamp, nonce, callback, HMACSHA1SignatureType, out normalizedUrl, out normalizedRequestParameters);

                    HMACSHA1 hmacsha1 = new HMACSHA1();
                    hmacsha1.Key = Encoding.ASCII.GetBytes(string.Format("{0}&{1}", UrlEncode(consumerSecret), string.IsNullOrEmpty(tokenSecret) ? "" : UrlEncode(tokenSecret)));

                    return GenerateSignatureUsingHash(signatureBase, hmacsha1);
                case SignatureTypes.RSASHA1:
                    throw new NotImplementedException();
                default:
                    throw new ArgumentException("Unknown signature type", "signatureType");
            }
        }

        public string GenerateSignatureBase(Uri url, string consumerKey, string token, string tokenSecret, string httpMethod, string timeStamp, string nonce, string callback, string signatureType, out string normalizedUrl, out string normalizedRequestParameters)
        {
            if (token == null)
            {
                token = string.Empty;
            }

            if (tokenSecret == null)
            {
                tokenSecret = string.Empty;
            }

            if (string.IsNullOrEmpty(consumerKey))
            {
                throw new ArgumentNullException("consumerKey");
            }

            if (string.IsNullOrEmpty(httpMethod))
            {
                throw new ArgumentNullException("httpMethod");
            }

            if (string.IsNullOrEmpty(signatureType))
            {
                throw new ArgumentNullException("signatureType");
            }

            normalizedUrl = null;
            normalizedRequestParameters = null;

            List<QueryParameter> parameters = GetQueryParameters(url.Query);
            parameters.Add(new QueryParameter(OAuthVersionKey, OAuthVersion));
            parameters.Add(new QueryParameter(OAuthNonceKey, nonce));
            parameters.Add(new QueryParameter(OAuthTimestampKey, timeStamp));
            parameters.Add(new QueryParameter(OAuthSignatureMethodKey, signatureType));
            parameters.Add(new QueryParameter(OAuthConsumerKeyKey, consumerKey));

            //TODO: Make this less of a hack
            if (!string.IsNullOrEmpty(callback))
            {
                parameters.Add(new QueryParameter(OAuthCallbackKey, UrlEncode(callback)));
            }

            if (!string.IsNullOrEmpty(token))
            {
                parameters.Add(new QueryParameter(OAuthTokenKey, token));
            }

            if (!string.IsNullOrEmpty(oauth_verifier))
            {
                parameters.Add(new QueryParameter("oauth_verifier", this.Verifier));
            }

            parameters.Sort(new QueryParameterComparer());

            normalizedUrl = string.Format("{0}://{1}", url.Scheme, url.Host);
            if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443)))
            {
                normalizedUrl += ":" + url.Port;
            }
            normalizedUrl += url.AbsolutePath;
            normalizedRequestParameters = NormalizeRequestParameters(parameters);

            StringBuilder signatureBase = new StringBuilder();
            signatureBase.AppendFormat("{0}&", httpMethod.ToUpper());
            signatureBase.AppendFormat("{0}&", UrlEncode(normalizedUrl));
            signatureBase.AppendFormat("{0}", UrlEncode(normalizedRequestParameters));

            return signatureBase.ToString();
        }

        private List<QueryParameter> GetQueryParameters(string parameters)
        {
            if (parameters.StartsWith("?"))
            {
                parameters = parameters.Remove(0, 1);
            }

            List<QueryParameter> result = new List<QueryParameter>();

            if (!string.IsNullOrEmpty(parameters))
            {
                string[] p = parameters.Split('&');
                foreach (string s in p)
                {
                    if (!string.IsNullOrEmpty(s) && !s.StartsWith(OAuthParameterPrefix))
                    {
                        if (s.IndexOf('=') > -1)
                        {
                            string[] temp = s.Split('=');
                            result.Add(new QueryParameter(temp[0], temp[1]));
                        }
                        else
                        {
                            result.Add(new QueryParameter(s, string.Empty));
                        }
                    }
                }
            }

            return result;
        }
    }
}
